iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0

在 K8S 裡,不論是建立 Pod、修改 Deployment,還是讀取 Secret 等資源,大多都需要與 API Server 進行互動。

API Server 可以想成 K8S Control Plane 的主要入口。當我們送出建立或修改資源的請求後,API Server 會處理請求,並將 K8S 的資源狀態保存到 etcd。接著,Scheduler、Controller 等元件會持續觀察這些資源的狀態,讓實際狀態逐漸符合我們所宣告的期望狀態。

但這時候就出現一個很重要的問題:

既然 K8S API 可以控制 Cluster 裡的資源,那 API Server 怎麼知道「現在是誰在呼叫我」?

總不能任何人只要找到 API Server,就可以隨意建立 Pod、讀取 Secret,甚至修改整個 Cluster。

因此,在存取 K8S API 時,「身分」就變得非常重要。

人類使用者需要有自己的身分,而執行在 K8S 裡面的 Pod / Workload,同樣需要一個可以被辨識的身分。

這時候就要認識今天的主角:

ServiceAccount。

甚麼是 ServiceAccount ?

一起先來看看 K8S 在官方網站是如何介紹 ServiceAccount:
lab-01
K8S官方文件:Service Accounts

官方文件將 ServiceAccount 定義為一種非人類帳號(non-human account),用來提供 Kubernetes 叢集中的身分。Pod 中的應用程式或其他自動化程式,可以使用它的憑證向 API Server 驗證身分。

ServiceAccount 具有以下三個特性:

  • 範圍性:每一個 ServiceAccount 都會綁定在某一個 Kubernetes Namespace 中。每當建立新的 Namespace 時,Kubernetes 都會自動建立一個名為 default 的 ServiceAccount。
  • 輕便性:ServiceAccount 本身就是 Kubernetes API 中的一種 Object,因此可以透過 Kubernetes Manifest 快速建立,不需要另外建立一套外部使用者帳號系統。
  • 可攜性:一套 Workload 的設定可以將 ServiceAccount 定義與 Deployment、RBAC 等設定一起保存。因為 ServiceAccount 本身是輕量的 Kubernetes Object,而且身分以 Namespace 為範圍,所以在將整套 Workload 部署到另一個 Kubernetes 環境時,可以把 ServiceAccount 的設定一起部署。

了解 ServiceAccount 的基本特性後,接著透過實作觀察它的憑證與身分。

在容器實作使用 Service Account

1. 找到容器內部 Service Account 的檔案

先進入 Pod 裡面使用以下指令:

ls -l /var/run/secrets/kubernetes.io/serviceaccount/ 

lab-02

先來介紹一下 /var/run/secrets/kubernetes.io/serviceaccount/ 這個路徑,一樣可以在 K8S 的官方文件看到對這個路徑的說明:
lab-03

預設情況下,每一個 Pod 都會關聯到一個 ServiceAccount,而該 ServiceAccount 的憑證(Token)會被放置到 Pod 中每一個 Container 的檔案系統內,路徑就是 /var/run/secrets/kubernetes.io/serviceaccount/

若 Pod 或 ServiceAccount 停用了自動掛載,例如設定 automountServiceAccountToken: false,就不能假設容器內一定存在這個 Token 檔案。

在這邊看到的三個檔案分別的作用是:

  • token:ServiceAccount 的 Bearer Token,用來向 API Server 做 Authentication。
  • ca.crt:CA 憑證,用來驗證 Kubernetes API Server 的 TLS 憑證。
  • namespace:目前 Pod 所在的 Namespace。

當攻擊者取得 Pod 內的執行能力後,掛載其中的 ServiceAccount Token 可能成為優先檢查的目標之一。

2. 查看這個 Token 的身分:

接著來看一下現在這個 Pod 的 Namespace 跟 Token 的身分資訊,使用以下指令查看:

cat /var/run/secrets/kubernetes.io/serviceaccount/namespace; echo
cut -d. -f2 /var/run/secrets/kubernetes.io/serviceaccount/token | tr '_-' '/+' | base64 -d 2>/dev/null; echo

lab-04

小提醒:JWT 的 Payload 使用不含補齊符號 = 的 Base64URL 編碼。使用一般 base64 -d 解碼時,部分工具需要先轉換字元並補齊 =,否則可能出現錯誤或輸出不完整。以下指令會先完成這些處理,再進行解碼。

payload=$(cut -d. -f2 /var/run/secrets/kubernetes.io/serviceaccount/token |
  tr '_-' '/+')

case $((${#payload} % 4)) in
  2) payload="${payload}==" ;;
  3) payload="${payload}=" ;;
esac

printf '%s' "$payload" | base64 -d

目前 Pod 所在的 Namespace 為 lab20,下面的輸出稍微整理一下格式:

{
  "aud": [
    "https://kubernetes.default.svc.cluster.local"
  ],
  "exp": 1822547973,
  "iat": 1791011973,
  "iss": "https://kubernetes.default.svc.cluster.local",
  "jti": "f2d06360-3656-4124-98ef-d28360925dac",
  "kubernetes.io": {
    "namespace": "lab20",
    "node": {
      "name": "css-leon-12-183-demo",
      "uid": "4f52641f-2831-49a9-a8fb-f991a5871854"
    },
    "pod": {
      "name": "victim",
      "uid": "b3ded702-556a-401f-b9d0-139b87d11e4f"
    },
    "serviceaccount": {
      "name": "default",
      "uid": "c15b52c3-6c0c-4b0a-9305-85357282e2bf"
    },
    "warnafter": 1791015580
  },
  "nbf": 1791011973,
  "sub": "system:serviceaccount:lab20:default"
}

從這個 JSON 可以獲得幾個重要訊息:

  • 這個 Pod 名字叫做 "victim"。
  • Token 中記錄的 Node 名稱為 "css-leon-12-183-demo"。
  • 我們的 ServiceAccount 是 default。
  • 這個 Pod 所在的 Namespace 是 lab20。
  • 這個 Token 的時效性。

前面的解碼只能查看 Token 中的宣告,不能證明它目前有效。接著使用這個 Token 呼叫 SelfSubjectReview,確認 API Server 實際辨識到的呼叫者身分:

SA=/var/run/secrets/kubernetes.io/serviceaccount
curl -s --cacert $SA/ca.crt -H "Authorization: Bearer $(cat $SA/token)" \
  -H 'Content-Type: application/json' -X POST \
  https://kubernetes.default.svc/apis/authentication.k8s.io/v1/selfsubjectreviews \
  -d '{"apiVersion":"authentication.k8s.io/v1","kind":"SelfSubjectReview"}'

lab-05

這邊補上完整的 JSON 內容:

{
  "kind": "SelfSubjectReview",
  "apiVersion": "authentication.k8s.io/v1",
  "metadata": {
    "creationTimestamp": "2026-10-03T14:10:51Z"
  },
  "status": {
    "userInfo": {
      "username": "system:serviceaccount:lab20:default",
      "uid": "c15b52c3-6c0c-4b0a-9305-85357282e2bf",
      "groups": [
        "system:serviceaccounts",
        "system:serviceaccounts:lab20",
        "system:authenticated"
      ],
      "extra": {
        "authentication.kubernetes.io/credential-id": [
          "JTI=03581abb-85d6-4660-835f-b5dd5c0bd3f7"
        ],
        "authentication.kubernetes.io/node-name": [
          "css-leon-12-183-demo"
        ],
        "authentication.kubernetes.io/node-uid": [
          "4f52641f-2831-49a9-a8fb-f991a5871854"
        ],
        "authentication.kubernetes.io/pod-name": [
          "victim"
        ],
        "authentication.kubernetes.io/pod-uid": [
          "b3ded702-556a-401f-b9d0-139b87d11e4f"
        ]
      }
    }
  }
}

前者可以查看 Token 中宣告的身分與時間資訊,後者則透過 SelfSubjectReview,確認 API Server 實際辨識到的呼叫者身分。兩者搭配,有助於理解 Token 的內容,以及它被用來呼叫 API 時的身分辨識結果。

接著測試目前身分是否有權列出 lab20 Namespace 中的 Secret。

curl -s -o /dev/null -w "list secrets -> HTTP %{http_code}\n" --cacert $SA/ca.crt \
  -H "Authorization: Bearer $(cat $SA/token)" \
  https://kubernetes.default.svc/api/v1/namespaces/lab20/secrets

lab-06
前面的 SelfSubjectReview 已確認 API Server 辨識到的 ServiceAccount 身分,但列出 lab20 命名空間內 Secret 的請求仍被拒絕。這說明「身分驗證成功」不等於「具有操作資源的權限」,後者還需要經過授權判斷。


ServiceAccount 是身分,ServiceAccount Token 則是證明該身分的憑證。本次透過 Bearer Token 向 API Server 驗證身分,但通過驗證後能操作哪些資源,仍取決於授權設定,例如下一篇要介紹的 RBAC。

感謝大家今天的閱讀,這個系列已經來到第 20 天了。無論你是第一次讀到我的文章,還是一路追蹤到現在,都很謝謝你的支持。明天接著討論 K8s 的 RBAC,我們明天見!


上一篇
Day 19|換成攻擊者視角看 Kubernetes:Cluster 裡到底有哪些攻擊面?
下一篇
Day 21|RBAC 設錯會有多嚴重?從權限列舉到權限擴張
系列文
我以前被社會打穿,現在輪到我研究怎麼把系統打穿:資安工程師的 30 天紅隊轉職實驗 共 21 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言